Skip to content

[release-4.18] OCPBUGS-86726: Prevent SSRF via FQDN-typed EndpointSlices - #821

Open
MrSanketkumar wants to merge 2 commits into
openshift:release-4.18from
MrSanketkumar:CVE-2026-42965-4.18
Open

[release-4.18] OCPBUGS-86726: Prevent SSRF via FQDN-typed EndpointSlices#821
MrSanketkumar wants to merge 2 commits into
openshift:release-4.18from
MrSanketkumar:CVE-2026-42965-4.18

Conversation

@MrSanketkumar

@MrSanketkumar MrSanketkumar commented Jul 22, 2026

Copy link
Copy Markdown

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices. This allowed the usage of invalid EndpointSlices to target hostnames that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

  • RFE-2832 to implement support for ExternalName was considered and rejected due to security concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
  • OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability and not the need to support FQDN-based names
  • The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
  • The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname) is unespecified by Kubernetes and has no usage.
  • The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array is added to guarantee that no invalid nor restricted IP is used.

Backported : #820

Summary by CodeRabbit

  • New Features

    • Added filtering for invalid, restricted, and cloud metadata endpoint IP addresses.
    • Added support for optional extended route validation.
    • Unsupported endpoint address types are now skipped safely.
  • Bug Fixes

    • Prevented invalid backend addresses from reaching routing components.
    • Preserved valid endpoints when individual addresses are rejected.
    • Avoided modifying original endpoint data during validation.
  • Tests

    • Added comprehensive coverage for endpoint filtering, address types, route validation, and IPv4/IPv6 handling.

rikatz and others added 2 commits July 22, 2026 17:06
The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices.
This allowed the usage of invalid EndpointSlices to target hostnames
that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations
on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

* RFE-2832 to implement support for ExternalName was considered and rejected due to security
concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
* OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability
and not the need to support FQDN-based names
* The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
* The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname)
is unespecified by Kubernetes and has no usage.
* The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array
is added to guarantee that no invalid nor restricted IP is used.
@openshift-ci-robot openshift-ci-robot added jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. labels Jul 22, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@MrSanketkumar: This pull request references Jira Issue OCPBUGS-86726, which is invalid:

  • expected dependent Jira Issue OCPBUGS-86727 to be in one of the following states: VERIFIED, RELEASE PENDING, CLOSED (ERRATA), CLOSED (CURRENT RELEASE), CLOSED (DONE), CLOSED (DONE-ERRATA), but it is ASSIGNED instead

Comment /jira refresh to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

The OpenShift Router previously did not validate backend destinations resolved from FQDN-typed EndpointSlices. This allowed the usage of invalid EndpointSlices to target hostnames that resolves to restricted IPs (like the cloud metadata service at 169.254.169.254).

This commit disables the usage of EndpointSlices of type FQDN, and add validations on Endpoints to check if a restricted IP is being used before adding them to HAProxy endpoints Backend.

The implementation and disabling the usage of FQDN-backed endpoints is based on the following:

  • RFE-2832 to implement support for ExternalName was considered and rejected due to security concerns, so OpenShift today does not support officially the usage of FQDN-based endpoints on router
  • OCPBUGS-55506 relates to a mistake caused by the customer that caused unavailability and not the need to support FQDN-based names
  • The behavior of an endpointslice of type FQDN is deprecated on Kubernetes
  • The behavior of an endpointslice using an address that is not IPv4 or IPv6 (eg.: hostname) is unespecified by Kubernetes and has no usage.
  • The hostname field on endpointslice is not used on router

This way, there is a common understanding that the usage of FQDN based addresses on Router was a mistake, and disabling it is the right fix.

Additional validations of the IP address on the generated endpointnt array is added to guarantee that no invalid nor restricted IP is used.

Backported : #820

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2c6dabab-88ca-41b3-be61-0418276ce065

📥 Commits

Reviewing files that changed from the base of the PR and between e5c059e and b2e4b66.

⛔ Files ignored due to path filters (25)
  • vendor/github.com/pmezard/go-difflib/LICENSE is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/pmezard/go-difflib/difflib/difflib.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/LICENSE is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_compare.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_format.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_forward.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertion_order.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/assertions.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/doc.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/errors.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/forward_assertions.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/http_assertions.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/doc.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/forward_requirements.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/require.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/require.go.tmpl is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/require_forward.go is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/require_forward.go.tmpl is excluded by !**/vendor/**, !vendor/**
  • vendor/github.com/stretchr/testify/require/requirements.go is excluded by !**/vendor/**, !vendor/**
  • vendor/modules.txt is excluded by !**/vendor/**, !vendor/**
📒 Files selected for processing (9)
  • go.mod
  • pkg/cmd/infra/router/template.go
  • pkg/router/controller/endpointsubset/converter.go
  • pkg/router/controller/endpointsubset/converter_test.go
  • pkg/router/controller/extended_validator.go
  • pkg/router/controller/extended_validator_test.go
  • pkg/router/controller/factory/factory_endpointslices_test.go
  • pkg/router/router_test.go
  • pkg/router/template/plugin_test.go

Walkthrough

Changes

Endpoint validation

Layer / File(s) Summary
EndpointSlice address-type handling
pkg/router/controller/endpointsubset/converter.go, pkg/router/controller/endpointsubset/converter_test.go, pkg/router/controller/factory/factory_endpointslices_test.go, pkg/router/router_test.go
Unsupported EndpointSlice address types are logged and skipped; supported address types are covered by conversion tests and explicit IPv4 fixtures.
Restricted endpoint IP filtering
pkg/router/controller/extended_validator.go, pkg/router/controller/extended_validator_test.go, go.mod
ExtendedValidator filters invalid and restricted IPs from endpoint subsets without mutating input objects, while retaining deletion pass-through behavior. IP validation and conversion cases are tested.
Configurable route validation
pkg/cmd/infra/router/template.go, pkg/router/controller/extended_validator.go, pkg/router/template/plugin_test.go
The validator is always installed with the configured route-validation flag, and route validation runs only when enabled.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EndpointSlice
  participant ConvertEndpointSlice
  participant ExtendedValidator
  participant router.Plugin
  EndpointSlice->>ConvertEndpointSlice: provide AddressType and endpoint addresses
  ConvertEndpointSlice->>ExtendedValidator: forward converted Endpoints
  ExtendedValidator->>ExtendedValidator: validate and filter endpoint IPs
  ExtendedValidator->>router.Plugin: forward filtered Endpoints
Loading

Suggested reviewers: rfredette, frobware


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Stable And Deterministic Test Names ❌ Error New subtest titles include numeric IP/CIDR literals like '127.0.0.0/8', making names overly specific and noncompliant. Rename subtests to generic stable phrases (e.g. 'loopback address in reserved range') and avoid numeric IP/CIDR literals or empty t.Run names.
No-Sensitive-Data-In-Logs ❌ Error New logs emit EndpointSlice namespace/name, route namespace/name, and backend IPs, which can expose internal/customer data. Remove or redact those fields from logs, or replace them with generic messages/hashes that don’t include resource names or IPs.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (12 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately highlights the main change: blocking FQDN-typed EndpointSlices to prevent SSRF.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Structure And Quality ✅ Passed No Ginkgo tests or indefinite waits were added; the new table-driven unit tests are focused and the existing endpoint-slice setup includes cleanup/timeouts.
Microshift Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changed tests are unit tests and reference no MicroShift-unsupported OpenShift APIs/features.
Single Node Openshift (Sno) Test Compatibility ✅ Passed PASS: The added tests are standard testing.T unit/integration tests; no Ginkgo/e2e specs, node-count assumptions, or SNO-sensitive topology checks.
Topology-Aware Scheduling Compatibility ✅ Passed The PR only changes router validation and EndpointSlice conversion/tests; no pod specs, replicas, affinity, node selectors, or topology-aware scheduling logic were added.
Ote Binary Stdout Contract ✅ Passed No new stdout writes were added in any process-level entrypoint; the new logging uses klogr/klog, which defaults to stderr.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No new Ginkgo e2e tests were added; the changed tests are unit tests using testing.T, with no external connectivity or IPv6-hostile host/URL patterns.
No-Weak-Crypto ✅ Passed Changed code adds endpoint validation and vendored test libs; no MD5/SHA1/DES/RC4/3DES/Blowfish/ECB or custom crypto appears, and no secret/token compares were added.
Container-Privileges ✅ Passed HEAD only changes go.mod and vendored deps; no container/K8s manifest changes or privileged settings were introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci
openshift-ci Bot requested review from frobware and rfredette July 22, 2026 11:40
@openshift-ci

openshift-ci Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign candita for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@MrSanketkumar

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MrSanketkumar

Copy link
Copy Markdown
Author

/retest-required

@openshift-ci

openshift-ci Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@MrSanketkumar: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@UdayYendva

Copy link
Copy Markdown

Cluster Version

oc get clusterversion

Output

NAME      VERSION                                                 AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.18.50-0-2026-07-23-143647-test-ci-ln-5phm0b2-latest   True        False         5m45s   Cluster version is 4.18.50-0-2026-07-23-143647-test-ci-ln-5phm0b2-latest

SSRF Protection Validation - EndpointSlice Verification Results

Test Environment Setup

Create test namespace and service

oc create namespace ssrf-test

oc -n ssrf-test create service clusterip metadata-svc --tcp=80:80

oc -n ssrf-test patch svc metadata-svc --type=json \
-p='[{"op":"remove","path":"/spec/selector"}]'

Create route

CLUSTER_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}')

oc -n ssrf-test expose svc metadata-svc \
--hostname=ssrf-test.${CLUSTER_DOMAIN}

Save URL

TEST_URL="http://ssrf-test.${CLUSTER_DOMAIN}/latest/meta-data/"

Test 1: FQDN EndpointSlice (Layer 1)

EndpointSlice

 cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-fqdn
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: FQDN
endpoints:
  - addresses:
      - "metadata.google.internal"
ports:
  - port: 80
EOF
Warning: spec.addressType: FQDN endpoints are deprecated
endpointslice.discovery.k8s.io/test-fqdn created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503
 
oc -n openshift-ingress logs deployment/router-default | grep "unsupported address type"pe"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
I0717 06:18:42.188302       1 converter.go:18] "msg"="Skipping EndpointSlice with unsupported address type" "addressType"="FQDN" "logger"="endpointsubset" "name"="test-fqdn" "namespace"="ssrf-test"

Test 2: IPv6 Hex Metadata IP (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-metadata-hex
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::a9fe:a9fe"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-metadata-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "a9fe"fe"
**Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:22:45.976337       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 169.254.169.254 is a restricted link-local IP" "address"="::a9fe:a9fe" "logger"="controller"**

Test 3: IPv6 Hex Loopback (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-loopback-hex
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::7f00:1"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-loopback-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "7f00"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:25:35.243202       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 127.0.0.1 is a restricted loopback IP" "address"="::7f00:1" "logger"="controller"

Test 4: AWS IPv6 IMDS (Layer 2 Pass 1)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-aws-ipv6-imds
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "fd00:ec2::254"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-aws-ipv6-imds created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fd00"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:27:50.255380       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address fd00:ec2::254 is a restricted cloud metadata IP" "address"="fd00:ec2::254" "logger"="controller"

Test 5: IPv6 Loopback (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-loopback
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-loopback.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "::1"

Output


Reason:
::1 is a reserved IPv6 loopback address.

Router log:
No output because EndpointSlice was not created.

Test 6: IPv6 Link-Local (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-linklocal
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "fe80::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-linklocal.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fe80"

Output


Reason:
fe80::1 is a reserved IPv6 link-local address.

Router log:
No output because EndpointSlice was not created.

Test 7: IPv6 Multicast (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-multicast
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "ff02::1"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-multicast.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "ff02"

Output


Reason:
ff02::1 is a reserved multicast address.

Router log:
No output because EndpointSlice was not created.

Test 8: IPv6 Unspecified (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-ipv6-unspecified
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "::"
ports:
  - port: 80

Verification

oc apply -f test-ipv6-unspecified.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "unspecified"

Output


Reason:
:: is a reserved IPv6 unspecified address.

Router log:
No output because EndpointSlice was not created.

Test 9: Valid IPv6 Pass-Through

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: test-valid-ipv6
  namespace: ssrf-test
  labels:
    kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
  - addresses:
      - "2001:db8::1"
ports:
  - port: 80
EOF
endpointslice.discovery.k8s.io/test-valid-ipv6 created

Verification

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "2001"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp (EXPECTED)
Expected Results:
No log line found regarding this address being restricted.
The router accepts it as a valid backend.

Test 10: Normal Application Regression Test

oc -n ssrf-test new-app --image=openshift/hello-openshift --name=hello
oc -n ssrf-test expose svc hello --hostname=hello-test.${CLUSTER_DOMAIN}
# Wait for pod to be ready
oc -n ssrf-test wait --for=condition=ready pod -l app=hello --timeout=60s
--> Found container image 7af3297 (8 years old) from Docker Hub for "openshift/hello-openshift"

    * An image stream tag will be created as "hello:latest" that will track this image

--> Creating resources ...
    imagestream.image.openshift.io "hello" created
    deployment.apps "hello" created
    service "hello" created
--> Success
    Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
     'oc expose service/hello' 
    Run 'oc status' to view your app.
route.route.openshift.io/hello exposed

Verification

curl -s http://hello-test.${CLUSTER_DOMAIN}
Hello OpenShift!

Note

Kubernetes/OpenShift prevent the creation of EndpointSlices that reference special-purpose IP address ranges, including loopback (127.0.0.0/8, ::1/128), unspecified (0.0.0.0, ::), multicast (224.0.0.0/4, ff00::/8), and other non-routable address ranges such as link-local addresses (169.254.0.0/16, fe80::/10) because of the built-in validation implemented by Kubernetes/OpenShift.

Tests 5–8 are blocked by Kubernetes/OpenShift built-in EndpointSlice validation before reaching the router SSRF validation logic.

/verified by ci

@openshift-ci-robot openshift-ci-robot added the verified Signifies that the PR passed pre-merge verification criteria label Jul 23, 2026
@openshift-ci-robot

Copy link
Copy Markdown
Contributor

@UdayYendva: This PR has been marked as verified by ci.

Details

In response to this:

Cluster Version

oc get clusterversion

Output

NAME      VERSION                                                 AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.18.50-0-2026-07-23-143647-test-ci-ln-5phm0b2-latest   True        False         5m45s   Cluster version is 4.18.50-0-2026-07-23-143647-test-ci-ln-5phm0b2-latest

SSRF Protection Validation - EndpointSlice Verification Results

Test Environment Setup

Create test namespace and service

oc create namespace ssrf-test

oc -n ssrf-test create service clusterip metadata-svc --tcp=80:80

oc -n ssrf-test patch svc metadata-svc --type=json \
-p='[{"op":"remove","path":"/spec/selector"}]'

Create route

CLUSTER_DOMAIN=$(oc get ingresses.config cluster -o jsonpath='{.spec.domain}')

oc -n ssrf-test expose svc metadata-svc \
--hostname=ssrf-test.${CLUSTER_DOMAIN}

Save URL

TEST_URL="http://ssrf-test.${CLUSTER_DOMAIN}/latest/meta-data/"

Test 1: FQDN EndpointSlice (Layer 1)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-fqdn
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: FQDN
endpoints:
 - addresses:
     - "metadata.google.internal"
ports:
 - port: 80
EOF
Warning: spec.addressType: FQDN endpoints are deprecated
endpointslice.discovery.k8s.io/test-fqdn created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "unsupported address type"pe"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
I0717 06:18:42.188302       1 converter.go:18] "msg"="Skipping EndpointSlice with unsupported address type" "addressType"="FQDN" "logger"="endpointsubset" "name"="test-fqdn" "namespace"="ssrf-test"

Test 2: IPv6 Hex Metadata IP (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-metadata-hex
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::a9fe:a9fe"
ports:
 - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-metadata-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "a9fe"fe"
**Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:22:45.976337       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 169.254.169.254 is a restricted link-local IP" "address"="::a9fe:a9fe" "logger"="controller"**

Test 3: IPv6 Hex Loopback (Layer 2 Pass 2)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-loopback-hex
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::7f00:1"
ports:
 - port: 80
EOF
endpointslice.discovery.k8s.io/test-ipv6-loopback-hex created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "7f00"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:25:35.243202       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address 127.0.0.1 is a restricted loopback IP" "address"="::7f00:1" "logger"="controller"

Test 4: AWS IPv6 IMDS (Layer 2 Pass 1)

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-aws-ipv6-imds
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "fd00:ec2::254"
ports:
 - port: 80
EOF
endpointslice.discovery.k8s.io/test-aws-ipv6-imds created

Verification

curl -s -o /dev/null -w "%{http_code}" $TEST_URL
503

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fd00"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp
E0717 06:27:50.255380       1 extended_validator.go:70] "msg"="Skipping endpoint address with restricted or invalid IP" "error"="IP address fd00:ec2::254 is a restricted cloud metadata IP" "address"="fd00:ec2::254" "logger"="controller"

Test 5: IPv6 Loopback (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-loopback
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-loopback.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "::1"

Output


Reason:
::1 is a reserved IPv6 loopback address.

Router log:
No output because EndpointSlice was not created.

Test 6: IPv6 Link-Local (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-linklocal
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "fe80::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-linklocal.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "fe80"

Output


Reason:
fe80::1 is a reserved IPv6 link-local address.

Router log:
No output because EndpointSlice was not created.

Test 7: IPv6 Multicast (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-multicast
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "ff02::1"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-multicast.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "ff02"

Output


Reason:
ff02::1 is a reserved multicast address.

Router log:
No output because EndpointSlice was not created.

Test 8: IPv6 Unspecified (Layer 2 Pass 1)

EndpointSlice

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-ipv6-unspecified
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "::"
ports:
 - port: 80

Verification

oc apply -f test-ipv6-unspecified.yaml

curl -s -o /dev/null -w "%{http_code}" $TEST_URL

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "unspecified"

Output


Reason:
:: is a reserved IPv6 unspecified address.

Router log:
No output because EndpointSlice was not created.

Test 9: Valid IPv6 Pass-Through

EndpointSlice

cat <<EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
 name: test-valid-ipv6
 namespace: ssrf-test
 labels:
   kubernetes.io/service-name: metadata-svc
addressType: IPv6
endpoints:
 - addresses:
     - "2001:db8::1"
ports:
 - port: 80
EOF
endpointslice.discovery.k8s.io/test-valid-ipv6 created

Verification

oc -n openshift-ingress logs deployment/router-default | grep "restricted" | grep "2001"
Found 2 pods, using pod/router-default-b46d5f5bc-xs5jp (EXPECTED)
Expected Results:
No log line found regarding this address being restricted.
The router accepts it as a valid backend.

Test 10: Normal Application Regression Test

oc -n ssrf-test new-app --image=openshift/hello-openshift --name=hello
oc -n ssrf-test expose svc hello --hostname=hello-test.${CLUSTER_DOMAIN}
# Wait for pod to be ready
oc -n ssrf-test wait --for=condition=ready pod -l app=hello --timeout=60s
--> Found container image 7af3297 (8 years old) from Docker Hub for "openshift/hello-openshift"

   * An image stream tag will be created as "hello:latest" that will track this image

--> Creating resources ...
   imagestream.image.openshift.io "hello" created
   deployment.apps "hello" created
   service "hello" created
--> Success
   Application is not exposed. You can expose services to the outside world by executing one or more of the commands below:
    'oc expose service/hello' 
   Run 'oc status' to view your app.
route.route.openshift.io/hello exposed

Verification

curl -s http://hello-test.${CLUSTER_DOMAIN}
Hello OpenShift!

Note

Kubernetes/OpenShift prevent the creation of EndpointSlices that reference special-purpose IP address ranges, including loopback (127.0.0.0/8, ::1/128), unspecified (0.0.0.0, ::), multicast (224.0.0.0/4, ff00::/8), and other non-routable address ranges such as link-local addresses (169.254.0.0/16, fe80::/10) because of the built-in validation implemented by Kubernetes/OpenShift.

Tests 5–8 are blocked by Kubernetes/OpenShift built-in EndpointSlice validation before reaching the router SSRF validation logic.

/verified by ci

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. jira/severity-important Referenced Jira bug's severity is important for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. verified Signifies that the PR passed pre-merge verification criteria

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants